// 5.1 结构和操作符
/*
字面量：匹配一个具体字符，包括不用转义的和需要转义的。比如a匹配字符“a”，又比如\n匹配换行符，又比如\.匹配小数点。
字符组：匹配一个字符，可以是多种可能之一，比如[0-9]，表示匹配一个数字。也有\d的简写形式。
       另外还有反义字符组，表示可以是除了特定字符之外任何一个字符，比如 [^0-9]， 表示一个非数字字符，也有 \D 的简写形式。
  量词：表示一个字符连续出现，比如 a{1,3} 表示 "a" 字符连续出现 3 次。另外还有常见的简写形式，比如 a+ 表示 "a" 字符连续出现至少一次。
    锚：匹配一个位置，而不是字符。比如 ^ 匹配字符串的开头，又比如 \b 匹配单词边界，又比如 (?=\d) 表示数字前面的位置。
  分组：用括号表示一个整体，比如 (ab)+，表示 "ab" 两个字符连续出现多次，也可以使用非捕获分组 (?:ab)+。
  分支：多个子表达式多选一，比如 abc|bcd，表达式匹配 "abc" 或者 "bcd" 字符子串。反向引用，比如 \2，表示引用第 2 个分组。
*/
/*
操作符描述            操作符                优先级 
转义符                  \                    1
括号和方括号  (…)、(?:…)、(?=…)、(?!…)、[…]    2
量词限定符      {m}、{m,n}、{m,}、?、*、+      3
位置和序列         ^、$、\元字符、一般字符       4
管道符（竖杠）            |                    5
*/

// 5.2. 注意要点
// 5.2.1 匹配字符串整体问题
// 因为是要匹配整个字符串，我们经常会在正则前后中加上锚 ^ 和 $。
// 比如要匹配目标字符串 "abc" 或者 "bcd" 时，如果一不小心，就会写成 /^abc|bcd$/。
// 而位置字符和字符序列优先级要比竖杠高，应该写成 /^(abc|bcd)$/

// 5.2.2 量词连缀问题
// 假设，要匹配这样的字符串：
// 1. 每个字符为 "a、"b"、"c" 任选其一，
// 2. 字符串的长度是 3 的倍数。
// 此时正则不能想当然地写成 /^[abc]{3}+$/，这样会报错，说 + 前面没什么可重复的：
// 应该写成：/([abc]{3})+/

// 5.2.3 元字符转义问题
// 所谓元字符，就是正则中有特殊含义的字符。
// ^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、- ,
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
console.log(regex.test(string)); // => true
// 其中 string 中的 \ 字符也要转义的。
// 另外，在 string 中，也可以把每个字符转义，当然，转义后的结果仍是本身：
var string = "^$.*+?|\\/[]{}=!:-,";
var string2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";
console.log(string == string2); // => true
// 现在的问题是，是不是每个字符都需要转义呢？否，看情况。

// 5.2.3.1. 字符组中的元字符
// 跟字符组相关的元字符有 [、]、^、-。因此在会引起歧义的地方进行转义。例如开头的 ^ 必须转义，不然会把整个字符组，看成反义字符组。
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;
console.log(string.match(regex));
// => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]

// 5.2.3.2. 匹配 "[abc]" 和 "{3,5}"
// 我们知道 [abc]，是个字符组。如果要匹配字符串 "[abc]" 时，该怎么办？
// 可以写成 /\[abc\]/，也可以写成 /\[abc]/，测试如下：
var string = "[abc]";
var regex = /\[abc]/g;
console.log(string.match(regex)[0]); // => "[abc]"
// 只需要在第一个方括号转义即可，因为后面的方括号构不成字符组，正则不会引发歧义，自然不需要转义。
// 同理，要匹配字符串 "{3,5}"，只需要把正则写成 /\{3,5}/ 即可。
var string = "{3,5}";
var regex = /\{3,5}/g;
console.log(string.match(regex)[0]); // => "{3,5}"
// 另外，我们知道量词有简写形式 {m,}，却没有 {,n} 的情况。虽然后者不构成量词的形式，但此时并不会报错。当然，匹配的字符串也是 "{,n}"，测试如下：
var string = "{,3}";
var regex = /{,3}/g;
console.log(string.match(regex)[0]); // => "{,3}"

// 5.2.3.3. 其余情况
// 比如 =、!、:、-、, 等符号，只要不在特殊结构中，并不需要转义。
// 但是，括号需要前后都转义的，如 /\(123\)/。
// 至于剩下的 ^、$、.、*、+、?、|、\、/ 等字符，只要不在字符组内，都需要转义的。

// 5.3 案例分析
// 5.3.1 身份证
var regex = /^(\d{15}|\d{17}[\dxX])$/;
var string = "320830199509012213";
console.log(regex.test(string));

var p = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
//输出 true
console.log(p.test("11010519491231002X"));
//输出 false 不能以0开头
console.log(p.test("01010519491231002X"));
//输出 false 年份不能以17开头
console.log(p.test("11010517491231002X"));
//输出 false 月份不能为13
console.log(p.test("11010519491331002X"));
//输出 false 日期不能为32
console.log(p.test("11010519491232002X"));
//输出 false 不能以a结尾
console.log(p.test("11010519491232002a"));

// 判断身份证是否在省份范围
var checkProv = function(val) {
    var pattern = /^[1-9][0-9]/;
    var provs = { 11: "北京", 12: "天津", 13: "河北", 14: "山西", 15: "内蒙古", 21: "辽宁", 22: "吉林", 23: "黑龙江 ", 31: "上海", 32: "江苏", 33: "浙江", 34: "安徽", 35: "福建", 36: "江西", 37: "山东", 41: "河南", 42: "湖北 ", 43: "湖南", 44: "广东", 45: "广西", 46: "海南", 50: "重庆", 51: "四川", 52: "贵州", 53: "云南", 54: "西藏 ", 61: "陕西", 62: "甘肃", 63: "青海", 64: "宁夏", 65: "新疆", 71: "台湾", 81: "香港", 82: "澳门" };
    if (pattern.test(val)) {
        if (provs[val]) {
            return true;
        }
    }
    return false;
}
//输出 true，37是山东
console.log(checkProv(37));
//输出 false，16不存在
console.log(checkProv(16));

// 判断身份证日期
var checkDate = function(val) {
    var pattern = /^(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)$/;
    if (pattern.test(val)) {
        var year = val.substring(0, 4);
        var month = val.substring(4, 6);
        var date = val.substring(6, 8);
        var date2 = new Date(year + "-" + month + "-" + date);
        if (date2 && date2.getMonth() == (parseInt(month) - 1)) {
            return true;
        }
    }
    return false;
}
//输出 true
console.log(checkDate("20180212"));
//输出 false 2月没有31日
console.log(checkDate("20180231"));

// 验证校验码
var checkCode = function(val) {
    var p = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
    var factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
    var parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2];
    var code = val.substring(17);
    if (p.test(val)) {
        var sum = 0;
        for (var i = 0; i < 17; i++) {
            sum += val[i] * factor[i];
        }
        if (parity[sum % 11] == code.toUpperCase()) {
            return true;
        }
    }
    return false;
}
// 输出 true， 校验码相符
console.log(checkCode("11010519491231002X"));
// 输出 false， 校验码不符
console.log(checkCode("110105194912310021"));


// 身份证完整检验规则
var checkID = function(val) {
    if (checkCode(val)) {
        var date = val.substring(6, 14);
        if (checkDate(date)) {
            if (checkProv(val.substring(0, 2))) {
                return true;
            }
        }
    }
    return false;
}
//输出 true
console.log(checkID("11010519491231002X"));
//输出 false，校验码不符
console.log(checkID("110105194912310021"));
//输出 false，日期码不符
console.log(checkID("110105194902310026"));
//输出 false，地区码不符
console.log(checkID("160105194912310029"));

// 5.3.2 IPV4 地址  /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/